package com.nhncorp.mojo.jumble;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.codehaus.plexus.util.FileUtils;
import org.jdom.CDATA;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.IllegalDataException;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;

/**
 * 
 * @author NHN Corporation
 * @version $Rev$, $Date$
 * @goal mutation
 * @inheritByDefault false
 * @requiresProject
 */
public class JumbleMutationMojo extends AbstractMutationMojo {
	protected static final String NUMBER = "([0-9]+)%";
	protected static final String SCORE = "Score: ([\\w\\W]+)";
	protected static final String UNIT_TEST_TIME_LIMIT = "unit test time limit ([\\w\\W]+)";
	protected static final String MUTATION_POINTS = "Mutation points \\= ([0-9]+)";
	protected static final String EX_CLASS = ".class";
	protected int projectScore = 0;
	protected int packageCountInProject = 0;
	protected int packageScore = 0;
	protected int classCountInPackage = 0;
	protected int NO_TEST_CLASS = 0;
	protected int TEST_CLASS_IS_BROKEN = 0;
	protected int NO_MUTATIONS_POSSIBLE = 0;
	protected int INTERFACE = 0;
	public Element root = new Element("testquality");
	public Element packagesElement = new Element("packages");
	public String fileSeparator = System.getProperty("file.separator");
	public String pathSeparator = System.getProperty("path.separator");
	public String userDir = System.getProperty("user.dir");
	public StringBuffer classesDir = new StringBuffer(fileSeparator).append("target").append(fileSeparator).append("classes");
	public StringBuffer testClassesDir = new StringBuffer(fileSeparator).append("target").append(fileSeparator).append("test-classes");
	public StringBuffer absoluteClassDir = new StringBuffer(userDir).append(classesDir).append(fileSeparator);
	public List<String> targetClasses = new ArrayList<String>();

	@SuppressWarnings("unchecked")
	public void execute() throws MojoExecutionException, MojoFailureException {
		boolean executed = false;
		String packageName = "";
		Set dependencies = getDependencies();
		Iterator<Artifact> iterator = dependencies.iterator();
		StringBuffer classPath = new StringBuffer();
		Element packageElement = null;
		Element classesElement = null;

		while (iterator.hasNext()) {
			Artifact key = iterator.next();
			classPath.append(key.getFile().getAbsolutePath()).append(pathSeparator);
		}

		classPath.append(userDir).append(classesDir);
		classPath.append(pathSeparator).append(userDir).append(testClassesDir);

		executeTests(executed, packageName, classPath, packageElement, classesElement);
	}

	protected void executeTests(boolean executed, String packageName, StringBuffer classPath, Element packageElement, Element classesElement) {
		try {
			targetClasses = FileUtils.getFileNames(new File(absoluteClassDir.toString()), makeIncludeStringByComma(), makeExcludeStringByComma(), true);
			mutationTestEachClass(packageName, classPath.toString(), packageElement, classesElement);

			if (isXmlOutput()) {
				writeXmlOutput();
			}

			if (!executed) {
				getLog().info("Mutation Test is finished.");
			}

		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	protected String makeIncludeStringByComma() {
		StringBuffer includeString = new StringBuffer();

		if (includes == null) {
			return null;
		}

		for (String include : includes) {
			includeString.append(include).append(",");
		}

		return includeString.toString().substring(0, includeString.length() - 1);
	}

	protected String makeExcludeStringByComma() {
		StringBuffer excludeString = new StringBuffer();

		if (excludes == null) {
			return null;
		}

		for (String exclude : excludes) {
			excludeString.append(exclude).append(",");
		}

		return excludeString.toString().substring(0, excludeString.length() - 1);
	}

	protected void mutationTestEachClass(String packageName, String classPath, Element packageElement, Element classesElement) {
		String className;

		for (String fullClassName : targetClasses) {
			fullClassName = fullClassName.replace(absoluteClassDir, "").replace(EX_CLASS, "").replace(fileSeparator, ".");
			String tempPackageName = fullClassName.substring(0, fullClassName.lastIndexOf("."));
			className = fullClassName.substring(fullClassName.lastIndexOf(".") + 1, fullClassName.length());

			if (tempPackageName.equals(packageName) == false) {
				if (packageName.equals("") == false) {
					packageElement.addContent(classesElement);
					calculatePackageScore(packageElement);
					packagesElement.addContent(packageElement);
					calculateProjectScore();
					initPackageScoreData();
					initTestStatusVariable();
				}
				packageElement = new Element("package");
				packageElement.setAttribute("name", tempPackageName);
				classesElement = new Element("classes");
			}

			try {
				classesElement = startMutationTests(classesElement, classPath, fullClassName, className);
			} catch (IOException e) {
				e.printStackTrace();
			}
			packageName = tempPackageName;
		}
		packageElement.addContent(classesElement);
		calculatePackageScore(packageElement);
		packagesElement.addContent(packageElement);
		calculateProjectScore();

		root.setAttribute("score", (projectScore / packageCountInProject) + "%");
		root.addContent(packagesElement);
	}

	protected void calculatePackageScore(Element packageElement) {
		if (NO_TEST_CLASS == classCountInPackage) {

			packageElement.setAttribute("score", "0% (NO TEST CLASS)");
		} else if (TEST_CLASS_IS_BROKEN == classCountInPackage) {
			packageElement.setAttribute("score", "0% (TEST CLASS IS BROKEN)");
		} else if (NO_MUTATIONS_POSSIBLE == classCountInPackage) {
			packageElement.setAttribute("score", "100% (NO MUTATIONS POSSIBLE)");
		} else if (INTERFACE == classCountInPackage) {
			packageElement.setAttribute("score", "100% (INTERFACE)");
		} else {
			packageElement.setAttribute("score", (packageScore / classCountInPackage) + "%");
		}
	}

	protected void initTestStatusVariable() {
		NO_TEST_CLASS = 0;
		NO_MUTATIONS_POSSIBLE = 0;
		INTERFACE = 0;
		TEST_CLASS_IS_BROKEN = 0;
	}

	protected void initPackageScoreData() {
		packageScore = 0;
		classCountInPackage = 0;
	}

	protected void calculateProjectScore() {
		projectScore += packageScore / classCountInPackage;
		packageCountInProject++;
	}

	public String getMatchesGroup(String source, String regx, int group) {
		Pattern pattern = Pattern.compile(regx);
		Matcher matcher = pattern.matcher(source);

		if (matcher.find() == true) {
			return matcher.group(group);
		}

		return null;
	}

	protected Element startMutationTests(Element packageElement, String classPath, String targetClass, String className) throws IOException {
		StringBuffer command = commandLineSetting(classPath, targetClass);
		Process exec = runJumbleProcess(command);
		displayResult(exec, packageElement, className);

		return packageElement;
	}

	protected void displayResult(Process exec, Element packageElement, String className) {
		try {
			String consoleLog;
			BufferedReader bufferedReader = getConsoleLogBufferedReader(exec);
			Element classElement = new Element("class");
			Element failsElement = new Element("fails");
			Element verboseElement = new Element("verbose");
			Thread errorStreamThread = collectErrorStream(exec);

			classElement.setAttribute("name", className);
			classCountInPackage++;

			while ((consoleLog = bufferedReader.readLine()) != null) {
				getLog().info(consoleLog);
				if (isXmlOutput()) {
					if (consoleLog.equals("") == false) {
						makeXmlOutputContents(consoleLog, classElement, failsElement);
					}
				}
			}
			bufferedReader.close();

			waitThread(exec, errorStreamThread);
			compositeXmlStructure(packageElement, classElement, failsElement, verboseElement);

		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	protected void compositeXmlStructure(Element packageElement, Element classElement, Element failsElement, Element verboseElement) {
		if (isXmlOutput()) {
			String verbose = CollectErrorStream.verbose.toString();
			CDATA cdata = new CDATA("IllegalDataException!");
			if (verbose.isEmpty() == false) {
				try {
					cdata = new CDATA(verbose);
				} catch (IllegalDataException e) {
				}
				verboseElement.addContent(cdata);
			}
			classElement.addContent(verboseElement);
			classElement.addContent(failsElement);
			packageElement.addContent(classElement);
		}
	}

	protected void waitThread(Process exec, Thread errorStreamThread) throws InterruptedException {
		while (true) {
			if (!errorStreamThread.isAlive()) {
				exec.waitFor();
				break;
			}
		}
	}

	protected Thread collectErrorStream(Process exec) {
		CollectErrorStream gb1 = new CollectErrorStream(exec.getErrorStream(), isXmlOutput());
		Thread thread1 = new Thread(gb1);
		thread1.start();
		return thread1;
	}

	protected Process runJumbleProcess(StringBuffer command) throws IOException {
		Process exec = Runtime.getRuntime().exec(command.toString());
		return exec;
	}

	protected BufferedReader getConsoleLogBufferedReader(Process exec) throws UnsupportedEncodingException {
		InputStream inputStream = exec.getInputStream();
		BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));

		return bufferedReader;
	}

	protected boolean isXmlOutput() {
		return xmlOutput != null;
	}

	void makeXmlOutputContents(String consoleLog, Element classElement, Element failsElement) {
		if (consoleLog.startsWith("Mutating ")) {
		} else if (consoleLog.startsWith("Tests:")) {
		} else if (consoleLog.startsWith("Mutation points")) {
			setXmlAttrPointAndTime(consoleLog, classElement);
		} else if (consoleLog.contains("FAIL:")) {
			setXmlAttrLineAndMutantContentFail(consoleLog, failsElement);
		} else if (consoleLog.startsWith("Score:")) {
			setXmlAttrScore(consoleLog, classElement);
			addCountTestStatus(consoleLog);
		}
	}

	protected void addCountTestStatus(String consoleLog) {
		if (consoleLog.contains("NO TEST CLASS")) {
			NO_TEST_CLASS++;
		} else if (consoleLog.contains("TEST CLASS IS BROKEN")) {
			TEST_CLASS_IS_BROKEN++;
		} else if (consoleLog.contains("INTERFACE")) {
			INTERFACE++;
		} else if (consoleLog.contains("NO MUTATIONS POSSIBLE")) {
			NO_MUTATIONS_POSSIBLE++;
		}
	}

	protected void setXmlAttrScore(String consoleLog, Element classElement) {
		String score = getMatchesGroup(consoleLog, SCORE, 1);
		classElement.setAttribute("score", score);
		packageScore += Integer.parseInt(getMatchesGroup(score, NUMBER, 1));
	}

	protected void setXmlAttrLineAndMutantContentFail(String consoleLog, Element failsElement) {
		Element failElement = new Element("fail");
		String[] split = consoleLog.split(":");
		String line = split[2];
		String mutant = split[3];

		failElement.setAttribute("line", line);
		failElement.setAttribute("mutant", mutant.substring(1));

		failsElement.addContent(failElement);
	}

	protected void setXmlAttrPointAndTime(String consoleLog, Element classElement) {
		String point = getMatchesGroup(consoleLog, MUTATION_POINTS, 1);
		String time = getMatchesGroup(consoleLog, UNIT_TEST_TIME_LIMIT, 1);

		if (point != null) {
			classElement.setAttribute("point", point);
		} else {
			classElement.setAttribute("point", "0");
		}
		if (time != null) {
			classElement.setAttribute("time", time);
		} else {
			classElement.setAttribute("time", "0s");
		}
	}

	protected StringBuffer commandLineSetting(String classPath, String targetClass) {
		StringBuffer command = new StringBuffer("java ");

		command.append("-jar ");
		command.append(jumbleHome).append(fileSeparator).append("jumble.jar ");
		command.append(mutator == null ? "" : mutator).append(" ");
		command.append("--classpath ");
		command.append(classPath).append(" ");
		command.append(targetClass).append(" ");

		return command;
	}

	protected void writeXmlOutput() {
		Document document = new Document(root);
		FileOutputStream fileOutputStream = null;

		try {
			fileOutputStream = new FileOutputStream(xmlOutput);

			XMLOutputter outputter = new XMLOutputter();
			Format format = outputter.getFormat();
			format.setEncoding("UTF-8");
			format.setIndent("	");
			format.setLineSeparator("\r\n");
			outputter.setFormat(format);
			outputter.output(document, fileOutputStream);
			fileOutputStream.flush();
			fileOutputStream.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
